序言
若多个线程访问同一个可变的状态变量时未使用合适的同步,那么程序就会出现错误。
为了避免错误的发生,可通过以下 3 种方式来保证正确性:
- 不在线程之间共享该状态变量
- 将状态变量修改为不可变的变量
- 在访问状态变量时使用同步
简单来讲,上述做法都是为了保证多线程下程序的安全。
那么,什么是线程安全?
线程安全性
《Java 并发编程实战》一书中定义如下:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在 Java 中,无状态对象一定是线程安全的。
什么是无状态对象?可以简单理解为没有成员变量的类的对象。
若只对对象的局部变量进行操作,则该对象是线程安全的,因为线程栈上的局部变量只能由正在执行的线程访问。
三大安全特性
线程安全,可以体现在 3 个方面:
- 原子性:保证一个操作或多个操作要么全部执行要么全部不执行
- 可见性:多个线程访问同一共享数据的时候,若某一个线程修改了此共享数据,那么其他线程能够立即看到此数据的改变
- 有序性:程序执行的顺序按照代码的先后顺序执行
原子性
为了认识原子性,我们来看个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo1 {
private int count = 0;
public void add() {
count++;
}
public void test() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> add());
}
System.out.println(count);
executorService.shutdown();
}
}
若运行该程序,你会发现:最后 count 的输出结果并不是 10000 。
这是为什么呢?
虽然count++递增操作看上去是一个操作,但这个操作其实并非原子的,因此它并不会被作为一个不可分割的操作来执行。
实际上,count++包含了三个独立的操作:
① 读取count的值;
② 将值加 1
③ 将计算结果写入 count
这是一个读取——修改——写入的操作序列,对 10000 个线程而言,若它们中的某些线程在同一时间执行,结果就不正确。
从 JMM 层面来讲,以上操作可以用下图及下表来解释:

| 线程\时间 | t1 | t2 | t3 | t4 |
|---|---|---|---|---|
| 线程 A | 从主内存读取count到本线程 |
递增本线程count的值 |
将count写回主内存 |
|
| 线程 B | 从主内存读取count到本线程 |
递增本线程count的值 |
将count写回主内存 |
现在线程 A 和线程 B 同时处理一个共享变量count,最开始count为0,不同时刻下:
- 在
tl时刻线程 A 读取count值到本地变量countA - 在
t2时刻递增countA的值为 1 ,同时线程 B 读取count的值 0 到本地变量countB,此时countB的值为 0 (因为countA的值还没有被写入主内存) - 在
t3时刻线程 A 才把countA的值 1 写入主内存 , 至此线程 A 一次计数完毕,同时线程 B 递增countB的值为 1 - 在
t4时刻 线程 B 把countB的值 l 写入主内存,至此线程 B 一次计数完毕
可以看到:明明是两次计数,为何最后结果是 l 而不是 2 呢?
其实这就是共享变量的线程安全中的原子性问题。(这里先不考虑内存可见性问题 )
竞争条件
在计算机中,使用一个专有名词来描述上面的情况——竞争条件。
竞争冒险(race hazard)又名竞态条件、竞争条件(race condition):描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机。此词源自于两个信号试着彼此竞争,来影响谁先输出。
举例来说,若计算机中的两个进程同时试图修改一个共享内存的内容,在没有并发控制的情况下,最后的结果依赖于两个进程的执行顺序与时机。但若发生了并发访问冲突,则最后的结果是不正确的。
那么我们该如何保证对类似count变量的操作是原子性的呢?
在 Java 中,可以通过以下两种方式来解决竞争条件问题:
- 使用 Atomic 包中的
CAS技术 - 使用
synchronized同步机制或 Lock 的AQS技术
在讲解它们之前,先了解两个重要概念:悲观锁与乐观锁。
何谓悲观锁与乐观锁?
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能以场景而定说一种人好于另一种人。
悲观锁
定义:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改它,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
而在 Java 中,synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
定义:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。
在Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的 CAS 技术实现的。
Atomic 包中的 CAS 技术
在 Java 中的java.util.concurrent.atomic包中提供了许多原子操作相关的类,部分如下:
- AtomicInteger
- AtomicBoolean
- AtomicLong
- AtomicReference
- LongAdder
下面对前面的例子使用AtomicInteger重写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo1 {
private AtomicInteger count = new AtomicInteger(0);
public void add() {
count.getAndIncrement();
}
public void test() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> add());
}
System.out.println(count);
executorService.shutdown();
}
}
最后的输出结果为 10000,刚好符合预期。
为什么呢?
我们可以在AtomicInteger类的源码中找到答案:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
...
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
在AtomicInteger类中,实际存储的值是放在value中的,而value的值是在new该实例(对象)时构造器传入的参数值。除此之外,我们还获取了Unsafe类的实例,并且定义了valueOffset,而valueOffset又是通过unsafe的objectFieldOffset方法运用反射机制从Atomic类中获取value的偏移量,那么valueOffset其实就是记录value的偏移量的。
之后调用getAndIncrement方法时,会调用Unsafe类的getAndAddInt方法,该方法源码如下:1
2
3
4
5
6
7public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
v = getIntVolatile(obj, offset);
} while (!compareAndSwapInt(obj, offset, v, v + delta));
return v;
}
这里我们见到的compareAndSwapInt函数,即是 CAS缩写的由来。
那么先分析下getAndAddInt函数做了什么,其中:
Object obj:代表传递进来的对象,此处为AtomicInteger的实例long offset:代表传递进来的对象的值,即AtomicInteger实例的值int delta:增加的值,即1v:通过调用unsafe的getIntVolatile(obj, offset)获取值,这是个native方法,具体实现到 JDK 源码中,其实就是获取对象obj的offset偏移量处的值。obj说过了,它是AtomicInteger,而offset就是前面在AtomicInteger中提到的valueOffset,这样我们就从内存中获取到现在valueOffset处的值了。
现在重点来了,compareAndSwapInt(obj, offset, v, v + delta)该如何理解?
将其换成compareAndSwapInt(obj, offset, expect, update)就比较清楚啦!
意思是:若获取的对象obj偏移量offset的主内存偏移值v和expect(工作内存期望值)相等,就证明没有其他线程改变过这个变量,那么就更新它为update(更新的目标值),若这一步的 CAS 没有成功,则采用自旋的方式继续进行 CAS 操作。
举个例子,假设线程 A 和线程 B 同时(分别跑在不同 CPU 上)执行getAndIncrement操作:
AtomicInteger里的 value 原始值为 3,即主内存中AtomicInteger的 value 为 3,根据 Java 内存模型,线程 A 和线程 B 各自持有一份 value 的副本,值为 3- 线程 A 通过
getIntVolatile(objA, offset)方法拿到 value 值 3,此时线程 A 时间片结束被挂起 - 线程 B 也通过
getIntVolatile(objB, offset)方法获取到 value 值 3,运气好,线程 B 没有被挂起,并执行compareAndSwapInt方法比较内存值也为 3,成功修改内存值为 2 - 最后线程 A 恢复,执行
compareAndSwapInt方法比较,发现自己手里的值 3 和内存的值 2 不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了
读者可能有个疑问:CAS 操作取出乍一看这不也是两个步骤嘛?
其实不然,在 JNI 里是借助于一个 CPU 指令完成的,所以它还是原子操作。
CAS 的问题
ABA 问题
CAS 需要在操作值的时候检查下内存值有没有发生变化,若没有发生变化则更新,但是若一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是 CAS 的 ABA 问题。
该问题的常见解决思路是使用版本号:在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A。
目前在 JDK 的 atomic 包里提供了一个类AtomicStampedReference,其给每个变量的状态值都配置了一个时间戳,因此避免了 ABA 问题的产生。
该类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,若全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
上面我们说过若 CAS 不成功,则会原地自旋,因此若长时间自旋会给 CPU 带来非常大的执行开销。
扩展—— Unsafe 类
JUC 的 atomic 包中 70% 的类都使用到了 Unsafe 类,那么它有什么特点?它具体的方法又做了啥呢?
我们可以研究一下。
Unsafe
JDK 的 rt.jar 包中 的Unsafe类提供了硬件级别的原子性操作, Unsafe类中的方法都是native方法 ,它们使用 JNI 的方式访问本地 C++实现库。
下面我们来了解一下 Unsafe 提供的几个主要的方法以及编程时如何使用 Unsafe 类做一些事情。
源码剖析
让我们来看看Unsafe的部分源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86public final class Unsafe {
/**
* 返回所属类中指定的变量在(主)内存偏移地址, 该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用
* 如下代码使用 Unsafe 类获取变量 value 在 AtomicLong 对象中的内存偏移
* static {
* try {
* valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
* } catch (Exception ex) { throw new Error(ex); }
* }
* @param field
*/
public native long objectFieldOffset(Field field);
/**
* 比较对象 obj 中偏移量为 offset 的变量的值是否与 expect 相等。
* - 相等则使用 update 值更新, 然后返回 true
* - 否则不更新并返回 false
*
* @param obj 传递的进来的对象
* @param offset 偏移量
* @param expected 期望值
* @param update 更新值
* @return
*/
public final native boolean compareAndSwapLong(Object obj, long offset,
long expected,
long update);
/**
* 获取对象 obj 中 偏移量为 offset 的变量对应 volatile 语义的值
*
* @param obj 传递的进来的对象
* @param offset 偏移量
* @return
*/
public native long getLongVolatile(Object obj, long offset);
/**
* 设置 obj 对象中 offset 偏移类型为 long 的 field 的值为 value ,支持 volatile 语义
*
* @param obj 传递的进来的对象
* @param offset 偏移量
* @param value
*/
public native void putLongVolatile(Object obj, long offset, long value);
/**
* 获取对象 obj 中偏移 量为 offset 的变量 volatile 语义的当前值 , 并设置变量 volatile 语义的值为 newValue
* - 首先通过 getLongVolatile 方法获取当前变量的值,然后使用 CAS 原子操作设置新值 。
* - 这里使用 while 循环是考虑到,在多个线程同时调用的情况下 CAS 失败时需要重试
*
* @param obj 传递的进来的对象
* @param offset 偏移量
* @param newValue 新值
* @return v 原值
* @since 1.8
*/
public final long getAndSetLong(Object obj, long offset, long newValue) {
long v;
do {
v = getLongVolatile(obj, offset);
} while (!compareAndSwapLong(obj, offset, v, newValue));
return v;
}
/**
* 获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值 , 并设置变量值为原始值+addValue
* - 类似 getAndSetLong 的实现 ,只是这里进行 CAS 操作时使用了原始值+传递的增量
*
* @param obj 传递的进来的对象
* @param offset 偏移量
* @param addValue 增量值
* @return v 原值
* @since 1.8
*/
public final long getAndAddLong(Object obj, long offset, long addValue) {
long v;
do {
v = getLongVolatile(obj, offset);
} while (!compareAndSwapLong(obj, offset, v, v + addValue));
return v;
}
}
Unsafe 类的使用
类的使用不是 so easy 嘛,是滴,但是世界并非永远如此简单,我们可以先尝试下使用Unsafe:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class TestUnsafe {
// 1.获取 Unsafe 的实例
static final Unsafe unsafe = Unsafe.getUnsafe();
// 2.记录变量 state 在类 TestUnsafe 中的偏移值
private volatile long state = 0;
// 3.初始化变量
private static long stateOffset = 0;
static {
try {
// 4.获取 state 变量在类 TestUnsafe 中的偏移值
stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 1.创建实例
TestUnsafe testUnsafe = new TestUnsafe();
// 2.设置 state 的值为 1
boolean success = unsafe.compareAndSwapInt(testUnsafe, stateOffset, 0, 1);
System.out.println(success);
}
}
运行 main 方法中的代码,我们期望是输出 true,但是执行结果却是这样:1
2
3
4Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at cn.lovike.basicjava.thread.TestUnsafe.<clinit>(TestUnsafe.java:98)
为什么会这样?
想要找出原因,比如需要查看getUnsafe的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public final class Unsafe {
...
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
// ①
Class<?> caller = Reflection.getCallerClass();
// ②
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
...
}
public class VM {
...
// ③ 判断 paramClassLoader 是不是 Bootstrap 类加载器
public static boolean isSystemDomainLoader(ClassLoader loader) {
return loader == null;
}
...
}
代码 ① 中,将获取调用getUnsafe方法的Class对象,这里是TestUnsafe.class。
代码 ② 中,会判断是不是Bootstrap类加载器加载的 localClass,此处查看的是不是Bootstrap类加载器加载的TestUnsafe,很明显TestUnsafe是由AppClassLoader加载的,因此这里直接抛出异常。
上面这么做的目的是出于安全考虑,具体可以去看看类加载相关的双亲委派机制,这里就不多谈了。
如果我们真的想要实例化Unsafe类,其实也是可以做到的,使用反射就行了,下面我们再来写个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class TestUnsafe {
// 1.获取 Unsafe 的实例
private static Unsafe unsafe = null;
// 2.记录变量 state 在类 TestUnsafe 中的偏移值
private volatile long state = 0;
// 3.初始化变量
private static long stateOffset = 0;
static {
try {
// 4.反射获取 theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
// 5.获取 state 变量在类 TestUnsafe 中的偏移值
stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 1.创建实例
TestUnsafe testUnsafe = new TestUnsafe();
// 2.设置 state 的值为 1
// 具体意思是 ,如果 testUnsafe 对象中内存偏移量为 stateOffset 的 state 变量的值为 0,则更新该值为 1
boolean success = unsafe.compareAndSwapInt(testUnsafe, stateOffset, 0, 1);
System.out.println(success);
}
}
如上通过反射获取Unsafe的实例,运行后将输出true。
synchronized 同步机制或 Lock 的 AQS 技术
另一种解决线程竞争条件问题的方式是使用synchronized同步机制或Lock 的 AQS 技术。
我们先通过 synchronized关键字来重写前面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo1Sync {
private int count = 0;
public synchronized void add() {
count++;
}
public void test() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> add());
}
System.out.println(count);
executorService.shutdown();
}
}
我们再通过 Lock来重写前面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo1Lock {
private int count = 0;
private Lock lock = new ReentrantLock();
public void add() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void test() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> add());
}
System.out.println(count);
executorService.shutdown();
}
}
以上 2 种方式,count的运行结果都是 10000,为什么呢?
因为它们底层都使用了 AQS 技术,那么 AQS 是什么呢?
锁的底层支持—— AQS
AQS,全称AbstractQueuedSynchronizer,即抽象同步队列,它是实现同步器的基础组件, 并发包中锁的底层就是使用 AQS 实现的。
下图是 AQS 的类图结构:
由上图可知,AQS 是一个 FIFO 的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。
对Node里的变量而言:
thread变量:用来存放进入 AQS 队列里面的线程SHARED:用来标记该线程是获取共享资源时被阻塞挂起后放入 AQS 队列的EXCLUSIVE:用来标记线程是获取独占资源时被挂起后放入 AQS 队列的waitStatus:记录当前线程等待状态,可以为以下四种:SIGNAL:线程需要被唤醒CANCELLED:线程被取消了CONDITION:线程在条件队列里面等待PROPAGATE:释放共享资源时需要通知其他节点
prev:记录当前节点的前驱节点next:记录当前节点的后继节点
在 AQS 中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对 AQS 的不同实现来说,state代表的意义不同:
semaphore:state代表当前可用信号的个数CountDownlatch:state代表计数器的当前值ReentrantLock:state代表当前线程获取锁的可重入次数;ReentrantReadWriteLock读写锁:state高 16 位代表读状态(获取该锁的次数),低 16 位代表获取到写锁的线程的可重入次数
可见性
导致共享变量在线程间不可见的原因有:
- 多个线程交叉执行
- 重排序结合多个线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
在 Java 中,有 2 种方式来保证可见性:
volatilesynchronized
volatile
简单来讲,volatile关键字可以确保对一个变量 的更新对其他线程马上可见 。
当 一个变量被声明为volatile时:
- 线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存
- 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
具体而言,volatile通过加入内存屏障和禁止重排序优化来实现可见性的:
- 对
volatile变量写操作时,会在写操作后加入一条store屏障指令,会立即将本地内存中的共享变量值刷新到主内存 - 对
volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
synchronized
JMM 规定了synchronized:
- 对线程加锁时,将清空工作内存中共享变量的值,因此使用共享变量时需要从主存中重新读取最新值
- 对线程解锁前,必须把共享变量的最新值从工作内存刷新到主存(加锁和解锁为同一把锁)
由于这个规定,synchronized可以保证可见性,而我们从前文证明了synchronized还能保证原子性。
因此,synchronized既保证了原子性,又保证了可见性。
有序性
Java 内存模型中,允许编译器和处理器对指令重排序以提高运行性能 , 并且只会对不存在数据依赖性的指令重排序。
在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下却存在问题。
多线程下的问题
在谈及多线程中的问题之前,我们先看一个例子:1
2
3int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
在如上代码中,变量 c 的值依赖 a 和 b 的值,因此重排序后能保证 ③ 操作在 ② ① 之后,但是 ① ② 谁先执行就不一定了,这在单线程下不会出现问题,因为不会影响最终结果。
那么,在多线程下又会怎样呢?
再来看个例子:
1 | public class OrderlinessTest { |
首先这段代码里面的变量没有被声明为volatile的 ,也没有使用任何同步措施,所以在多线程下存在共享变量内存可见性问题 。
这里先不谈内存可见性问题 ,因为通过把变量声明为volatile的本身就可以避免指令重排序问题 。 这里先看看指令重排序会造成什么影响 。
如上代码在不考虑内存可见性问题的情况下一定会输出 4 嘛?
答案是不 一 定,为什么呢?
由于代码 ①②③④ 间不存在依赖关系, 所以写线程的代码 ③④ 可能被重排序为先执行 ④ 再执行 ③, 那么执行 ④ 后, 读线程可能已经执行了 ① 操作, 并且在 ③ 执行前开始执行 ② 操作 , 这时候输出结果为 0 而不是 4 。
如何保证有序性?
在 Java 中,重排序在多线程下会导致非预期的程序执行结果,而以下技术都可以在一定程度上保证有序性:
volatilesynchronizedLockJMM
volatile
对volatile而言:
- 写
volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后 - 读
volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前
synchronized 和 Lock
JMM
Java 内存模型中,具备一些先天的有序性,这是因为其具有happens-before原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于后面的操作
- 锁定规则:一个
unLock操作先行发生于后面对于同一个锁的lock操作 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作- 传递规则:若操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C
- 线程启动规则:Thread 对象的
start()方法先行发生于此线程的每一个动作 - 线程中断规则:对线程
interrupt()方法的调用先行发生于被中断线程的代码监测到中断事件的发生 - 线程终结规则:线程中所有的操作都先行发生于线程的终止之前
- 对象终结规则:一个对象的初始化完成先行发生于他的
finalize()方法的开始
参考
- Brian Goetz. Java 并发编程实战 [M]. 机械工业出版社,2018
- 翟陆续,薛宾田. Java 并发编程之美 [M]. 电子工业出版社,2018
- 维基百科——竞争冒险
- 面试必备之乐观锁与悲观锁
- Java CAS 原理剖析
- 面试必问的 CAS,你懂了吗?